/*
 * Copyright 2010-2025 Hyland Software, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.activiti.engine.impl.bpmn.behavior;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.activiti.bpmn.model.Activity;
import org.activiti.bpmn.model.BoundaryEvent;
import org.activiti.bpmn.model.CompensateEventDefinition;
import org.activiti.bpmn.model.FlowElement;
import org.activiti.bpmn.model.FlowNode;
import org.activiti.bpmn.model.Process;
import org.activiti.engine.ActivitiIllegalArgumentException;
import org.activiti.engine.delegate.BpmnError;
import org.activiti.engine.delegate.DelegateExecution;
import org.activiti.engine.delegate.ExecutionListener;
import org.activiti.engine.delegate.Expression;
import org.activiti.engine.delegate.event.ActivitiEventType;
import org.activiti.engine.delegate.event.impl.ActivitiEventBuilder;
import org.activiti.engine.impl.bpmn.helper.ErrorPropagation;
import org.activiti.engine.impl.cmd.CompleteTaskCmd;
import org.activiti.engine.impl.context.Context;
import org.activiti.engine.impl.delegate.ActivityBehavior;
import org.activiti.engine.impl.delegate.SubProcessActivityBehavior;
import org.activiti.engine.impl.interceptor.CommandContext;
import org.activiti.engine.impl.persistence.entity.ExecutionEntity;
import org.activiti.engine.impl.util.CollectionUtil;
import org.activiti.engine.impl.util.ProcessDefinitionUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Implementation of the multi-instance functionality as described in the BPMN 2.0 spec.
 *
 * Multi instance functionality is implemented as an {@link ActivityBehavior} that wraps the original {@link ActivityBehavior} of the activity.
 *
 * Only subclasses of {@link AbstractBpmnActivityBehavior} can have multi-instance behavior. As such, special logic is contained in the {@link AbstractBpmnActivityBehavior} to delegate to the
 * {@link MultiInstanceActivityBehavior} if needed.
 *


 */
public abstract class MultiInstanceActivityBehavior
    extends FlowNodeActivityBehavior
    implements SubProcessActivityBehavior {

    private static final long serialVersionUID = 1L;

    protected static final Logger LOGGER = LoggerFactory.getLogger(MultiInstanceActivityBehavior.class);

    // Variable names for outer instance(as described in spec)
    public static final String NUMBER_OF_INSTANCES = "nrOfInstances";
    public static final String NUMBER_OF_ACTIVE_INSTANCES = "nrOfActiveInstances";
    public static final String NUMBER_OF_COMPLETED_INSTANCES = "nrOfCompletedInstances";

    // Instance members
    protected Activity activity;
    protected AbstractBpmnActivityBehavior innerActivityBehavior;
    protected Expression loopCardinalityExpression;
    protected Expression completionConditionExpression;
    protected Expression collectionExpression;
    protected String collectionVariable;
    protected String collectionElementVariable;
    // default variable name for loop counter for inner instances (as described in the spec)
    protected String collectionElementIndexVariable = "loopCounter";

    private String loopDataOutputRef;
    private String outputDataItem;

    /**
     * @param activity
     *          The {@link Activity} which has multi instance behaviour
     * @param innerActivityBehavior
     *          The original {@link ActivityBehavior} of the activity that will be wrapped inside this behavior.
     */
    public MultiInstanceActivityBehavior(Activity activity, AbstractBpmnActivityBehavior innerActivityBehavior) {
        this.activity = activity;
        setInnerActivityBehavior(innerActivityBehavior);
    }

    public void execute(DelegateExecution execution) {
        if (getLocalLoopVariable(execution, getCollectionElementIndexVariable()) == null) {
            clearLoopDataOutputRef(execution);
            int nrOfInstances = 0;

            try {
                nrOfInstances = createInstances(execution);
            } catch (BpmnError error) {
                ErrorPropagation.propagateError(error, execution);
            }

            if (nrOfInstances == 0) {
                super.leave(execution);
            }
        } else {
            getCommandContext().getHistoryManager().recordActivityStart((ExecutionEntity) execution);

            innerActivityBehavior.execute(execution);
        }
    }

    private void clearLoopDataOutputRef(DelegateExecution execution) {
        if (hasLoopDataOutputRef()) {
            execution.setVariable(getLoopDataOutputRef(), new ArrayList<>());
        }
    }

    protected abstract int createInstances(DelegateExecution execution);

    protected void executeCompensationBoundaryEvents(FlowElement flowElement, DelegateExecution execution) {
        //Execute compensation boundary events
        Collection<BoundaryEvent> boundaryEvents = findBoundaryEventsForFlowNode(
            execution.getProcessDefinitionId(),
            flowElement
        );
        if (CollectionUtil.isNotEmpty(boundaryEvents)) {
            // The parent execution becomes a scope, and a child execution is created for each of the boundary events
            for (BoundaryEvent boundaryEvent : boundaryEvents) {
                if (CollectionUtil.isEmpty(boundaryEvent.getEventDefinitions())) {
                    continue;
                }

                if (boundaryEvent.getEventDefinitions().get(0) instanceof CompensateEventDefinition) {
                    ExecutionEntity childExecutionEntity = getCommandContext()
                        .getExecutionEntityManager()
                        .createChildExecution((ExecutionEntity) execution);
                    childExecutionEntity.setParentId(execution.getId());
                    childExecutionEntity.setCurrentFlowElement(boundaryEvent);
                    childExecutionEntity.setScope(false);

                    ActivityBehavior boundaryEventBehavior = ((ActivityBehavior) boundaryEvent.getBehavior());
                    boundaryEventBehavior.execute(childExecutionEntity);
                }
            }
        }
    }

    protected Collection<BoundaryEvent> findBoundaryEventsForFlowNode(
        final String processDefinitionId,
        final FlowElement flowElement
    ) {
        Process process = getProcessDefinition(processDefinitionId);

        // This could be cached or could be done at parsing time
        List<BoundaryEvent> results = new ArrayList<BoundaryEvent>(1);
        Collection<BoundaryEvent> boundaryEvents = process.findFlowElementsOfType(BoundaryEvent.class, true);
        for (BoundaryEvent boundaryEvent : boundaryEvents) {
            if (
                boundaryEvent.getAttachedToRefId() != null &&
                boundaryEvent.getAttachedToRefId().equals(flowElement.getId())
            ) {
                results.add(boundaryEvent);
            }
        }
        return results;
    }

    protected Process getProcessDefinition(String processDefinitionId) {
        return ProcessDefinitionUtil.getProcess(processDefinitionId);
    }

    // Intercepts signals, and delegates it to the wrapped {@link ActivityBehavior}.
    public void trigger(DelegateExecution execution, String signalName, Object signalData) {
        innerActivityBehavior.trigger(execution, signalName, signalData);
    }

    // required for supporting embedded subprocesses
    public void lastExecutionEnded(DelegateExecution execution) {
        //ScopeUtil.createEventScopeExecution((ExecutionEntity) execution);
        leave(execution);
    }

    // required for supporting external subprocesses
    public void completing(DelegateExecution execution, DelegateExecution subProcessInstance) throws Exception {}

    // required for supporting external subprocesses
    public void completed(DelegateExecution execution) throws Exception {
        leave(execution);
    }

    // Helpers
    // //////////////////////////////////////////////////////////////////////

    @SuppressWarnings("rawtypes")
    protected int resolveNrOfInstances(DelegateExecution execution) {
        if (loopCardinalityExpression != null) {
            return resolveLoopCardinality(execution);
        } else if (usesCollection()) {
            Collection collection = resolveAndValidateCollection(execution);
            return collection.size();
        } else {
            throw new ActivitiIllegalArgumentException("Couldn't resolve collection expression nor variable reference");
        }
    }

    @SuppressWarnings("rawtypes")
    protected void executeOriginalBehavior(DelegateExecution execution, int loopCounter) {
        if (usesCollection() && collectionElementVariable != null) {
            Collection collection = (Collection) resolveCollection(execution);

            Object value = null;
            int index = 0;
            Iterator it = collection.iterator();
            while (index <= loopCounter) {
                value = it.next();
                index++;
            }
            setLoopVariable(execution, collectionElementVariable, value);
        }

        execution.setCurrentFlowElement(activity);
        Context.getAgenda().planContinueMultiInstanceOperation((ExecutionEntity) execution);
    }

    @SuppressWarnings("rawtypes")
    protected Collection resolveAndValidateCollection(DelegateExecution execution) {
        Object obj = resolveCollection(execution);
        if (collectionExpression != null) {
            if (!(obj instanceof Collection)) {
                throw new ActivitiIllegalArgumentException(
                    collectionExpression.getExpressionText() + "' didn't resolve to a Collection"
                );
            }
        } else if (collectionVariable != null) {
            if (obj == null) {
                throw new ActivitiIllegalArgumentException("Variable " + collectionVariable + " is not found");
            }

            if (!(obj instanceof Collection)) {
                throw new ActivitiIllegalArgumentException("Variable " + collectionVariable + "' is not a Collection");
            }
        } else {
            throw new ActivitiIllegalArgumentException("Couldn't resolve collection expression nor variable reference");
        }
        return (Collection) obj;
    }

    protected Object resolveCollection(DelegateExecution execution) {
        Object collection = null;
        if (collectionExpression != null) {
            collection = collectionExpression.getValue(execution);
        } else if (collectionVariable != null) {
            collection = execution.getVariable(collectionVariable);
        }
        return collection;
    }

    protected boolean usesCollection() {
        return collectionExpression != null || collectionVariable != null;
    }

    protected boolean isExtraScopeNeeded(FlowNode flowNode) {
        return flowNode.getSubProcess() != null;
    }

    protected int resolveLoopCardinality(DelegateExecution execution) {
        // Using Number since expr can evaluate to eg. Long (which is also the default for Juel)
        Object value = loopCardinalityExpression.getValue(execution);
        if (value instanceof Number) {
            return ((Number) value).intValue();
        } else if (value instanceof String) {
            return Integer.valueOf((String) value);
        } else {
            throw new ActivitiIllegalArgumentException(
                "Could not resolve loopCardinality expression '" +
                loopCardinalityExpression.getExpressionText() +
                "': not a number nor number String"
            );
        }
    }

    protected boolean completionConditionSatisfied(DelegateExecution execution) {
        if (completionConditionExpression != null) {
            Object value = completionConditionExpression.getValue(execution);
            if (!(value instanceof Boolean)) {
                throw new ActivitiIllegalArgumentException(
                    "completionCondition '" +
                    completionConditionExpression.getExpressionText() +
                    "' does not evaluate to a boolean value"
                );
            }

            Boolean booleanValue = (Boolean) value;
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Completion condition of multi-instance satisfied: {}", booleanValue);
            }
            return booleanValue;
        }
        return false;
    }

    protected void setLoopVariable(DelegateExecution execution, String variableName, Object value) {
        execution.setVariableLocal(variableName, value);
    }

    protected Integer getLoopVariable(DelegateExecution execution, String variableName) {
        Object value = execution.getVariableLocal(variableName);
        DelegateExecution parent = execution.getParent();
        while (value == null && parent != null) {
            value = parent.getVariableLocal(variableName);
            parent = parent.getParent();
        }
        return (Integer) (value != null ? value : 0);
    }

    protected Integer getLocalLoopVariable(DelegateExecution execution, String variableName) {
        return (Integer) execution.getVariableLocal(variableName);
    }

    protected void removeLocalLoopVariable(DelegateExecution execution, String variableName) {
        execution.removeVariableLocal(variableName);
    }

    /**
     * Since no transitions are followed when leaving the inner activity, it is needed to call the end listeners yourself.
     */
    protected void callActivityEndListeners(DelegateExecution execution) {
        getCommandContext()
            .getProcessEngineConfiguration()
            .getListenerNotificationHelper()
            .executeExecutionListeners(activity, execution, ExecutionListener.EVENTNAME_END);
    }

    protected void logLoopDetails(
        DelegateExecution execution,
        String custom,
        int loopCounter,
        int nrOfCompletedInstances,
        int nrOfActiveInstances,
        int nrOfInstances
    ) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug(
                "Multi-instance '{}' {}. Details: loopCounter={}, nrOrCompletedInstances={},nrOfActiveInstances={},nrOfInstances={}",
                execution.getCurrentFlowElement() != null ? execution.getCurrentFlowElement().getId() : "",
                custom,
                loopCounter,
                nrOfCompletedInstances,
                nrOfActiveInstances,
                nrOfInstances
            );
        }
    }

    protected DelegateExecution getMultiInstanceRootExecution(DelegateExecution executionEntity) {
        DelegateExecution multiInstanceRootExecution = null;
        DelegateExecution currentExecution = executionEntity;
        while (currentExecution != null && multiInstanceRootExecution == null && currentExecution.getParent() != null) {
            if (currentExecution.isMultiInstanceRoot()) {
                multiInstanceRootExecution = currentExecution;
            } else {
                currentExecution = currentExecution.getParent();
            }
        }
        return multiInstanceRootExecution;
    }

    protected void dispatchActivityCompletedEvent(DelegateExecution execution) {
        ExecutionEntity executionEntity = (ExecutionEntity) execution;
        FlowElement flowElement = executionEntity.getCurrentFlowElement();

        getCommandContext()
            .getEventDispatcher()
            .dispatchEvent(
                ActivitiEventBuilder.createActivityEvent(
                    ActivitiEventType.ACTIVITY_COMPLETED,
                    executionEntity,
                    flowElement
                )
            );
    }

    // Getters and Setters
    // ///////////////////////////////////////////////////////////

    public Expression getLoopCardinalityExpression() {
        return loopCardinalityExpression;
    }

    public void setLoopCardinalityExpression(Expression loopCardinalityExpression) {
        this.loopCardinalityExpression = loopCardinalityExpression;
    }

    public Expression getCompletionConditionExpression() {
        return completionConditionExpression;
    }

    public void setCompletionConditionExpression(Expression completionConditionExpression) {
        this.completionConditionExpression = completionConditionExpression;
    }

    public Expression getCollectionExpression() {
        return collectionExpression;
    }

    public void setCollectionExpression(Expression collectionExpression) {
        this.collectionExpression = collectionExpression;
    }

    public String getCollectionVariable() {
        return collectionVariable;
    }

    public void setCollectionVariable(String collectionVariable) {
        this.collectionVariable = collectionVariable;
    }

    public String getCollectionElementVariable() {
        return collectionElementVariable;
    }

    public void setCollectionElementVariable(String collectionElementVariable) {
        this.collectionElementVariable = collectionElementVariable;
    }

    public String getCollectionElementIndexVariable() {
        return collectionElementIndexVariable;
    }

    public void setCollectionElementIndexVariable(String collectionElementIndexVariable) {
        this.collectionElementIndexVariable = collectionElementIndexVariable;
    }

    public void setInnerActivityBehavior(AbstractBpmnActivityBehavior innerActivityBehavior) {
        this.innerActivityBehavior = innerActivityBehavior;
        this.innerActivityBehavior.setMultiInstanceActivityBehavior(this);
    }

    public AbstractBpmnActivityBehavior getInnerActivityBehavior() {
        return innerActivityBehavior;
    }

    public String getLoopDataOutputRef() {
        return loopDataOutputRef;
    }

    public boolean hasLoopDataOutputRef() {
        return loopDataOutputRef != null && !loopDataOutputRef.trim().isEmpty();
    }

    public void setLoopDataOutputRef(String loopDataOutputRef) {
        this.loopDataOutputRef = loopDataOutputRef;
    }

    public String getOutputDataItem() {
        return outputDataItem;
    }

    public boolean hasOutputDataItem() {
        return outputDataItem != null && !outputDataItem.trim().isEmpty();
    }

    public void setOutputDataItem(String outputDataItem) {
        this.outputDataItem = outputDataItem;
    }

    protected void updateResultCollection(DelegateExecution childExecution, DelegateExecution miRootExecution) {
        if (miRootExecution != null && hasLoopDataOutputRef()) {
            Object loopDataOutputReference = miRootExecution.getVariableLocal(getLoopDataOutputRef());
            List<Object> resultCollection;
            if (loopDataOutputReference instanceof List) {
                resultCollection = (List<Object>) loopDataOutputReference;
            } else {
                resultCollection = new ArrayList<>();
            }
            resultCollection.add(getResultElementItem(childExecution));
            setLoopVariable(miRootExecution, getLoopDataOutputRef(), resultCollection);
        }
    }

    protected Object getResultElementItem(DelegateExecution childExecution) {
        return getResultElementItem(childExecution.getVariablesLocal());
    }

    protected CommandContext getCommandContext() {
        return Context.getCommandContext();
    }

    protected Object getResultElementItem(Map<String, Object> availableVariables) {
        if (hasOutputDataItem()) {
            return availableVariables.get(getOutputDataItem());
        } else {
            //exclude from the result all the built-in multi-instances variables
            //and loopDataOutputRef itself that may exist in the context depending
            // on the variable propagation
            List<String> resultItemExclusions = Arrays.asList(
                getLoopDataOutputRef(),
                getCollectionElementIndexVariable(),
                NUMBER_OF_INSTANCES,
                NUMBER_OF_COMPLETED_INSTANCES,
                NUMBER_OF_ACTIVE_INSTANCES
            );
            HashMap<String, Object> resultItem = new HashMap<>(availableVariables);
            resultItemExclusions.forEach(resultItem.keySet()::remove);
            return resultItem;
        }
    }

    protected void propagateLoopDataOutputRefToProcessInstance(ExecutionEntity miRootExecution) {
        if (hasLoopDataOutputRef()) {
            miRootExecution
                .getProcessInstance()
                .setVariable(getLoopDataOutputRef(), miRootExecution.getVariable(getLoopDataOutputRef()));
        }
    }
}
